package IC.lir;

import IC.AST.*;
import IC.BinaryOps;
import IC.LiteralTypes;
import IC.Symbols.Kind;
import IC.Symbols.Symbol;
import IC.Symbols.SymbolTable;
import IC.Symbols.SymbolTableKind;
import IC.Types.ClassType;
import IC.Types.MethodType;
import IC.Types.TypeTable;
import IC.UnaryOps;

import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * Created by IntelliJ IDEA.
 * User: nimrod
 * Date: 24/01/12
 * Time: 12:36
 * To change this template use File | Settings | File Templates.
 */
public class LIRTranslationVisitor implements Visitor {
    private List<String> stringLiteralsCode;
    private List<String> dispatchTables;
    
    private int uniqueId;
    private int registerId;

    private SymbolTable globalSymbolTable;
    private TypeTable typeTable;
    
    private String currentLoopBreakLabel;
    private String currentLoopContinueLabel;

    private Map<String, String> stringLiterals;


    public LIRTranslationVisitor(SymbolTable globalSymbolTable, TypeTable typeTable) {
        this.globalSymbolTable = globalSymbolTable;
        this.typeTable = typeTable;

        this.uniqueId = 0;
        this.registerId = 0;
        this.currentLoopBreakLabel = null;
        this.currentLoopContinueLabel = null;
        stringLiteralsCode = new LinkedList<String>();
        stringLiterals = new LinkedHashMap<String, String>();

        dispatchTables = new LinkedList<String>();
    }

    public void setLirNameForVarsAndParams(SymbolTable symTable) {
        if (symTable.isParamVarLirNameSet()) return;

        ASTNode node = symTable.getNode();
        if (node instanceof Method) {
            Method method = (Method)node;
            if (method.getOverriddenMethod() != null) {
                setLirNameForVarsAndParams(method.getOverriddenMethod().getMethodSymTable());
            }
        }

        for (Symbol sym : symTable.getEntries().values()) {
            if (sym.getKind() == Kind.Var) {
                uniqueId++;
                sym.setLirName("v" + uniqueId + sym.getName());
            }

            if (sym.getKind() == Kind.Parameter) {
                Formal formal = (Formal)sym.getNode();
                if (formal.getOverriddenFormal() != null) {
                    sym.setLirName(formal.getOverriddenFormal().getSymbolTableEntry().getLirName());
                } else {
                    uniqueId++;
                    sym.setLirName("p" + uniqueId + sym.getName());
                }
            }
        }

        for (SymbolTable childTable : symTable.getChildTables()) {
            setLirNameForVarsAndParams(childTable);
        }

        symTable.setParamVarLirNameSet(true);
    }

    @Override
    public Object visit(Program program) {
        List<String> programCode = new LinkedList<String>();

        // generate lir name for variables
        setLirNameForVarsAndParams(globalSymbolTable);

        // generate lir name for methods
        for (ICClass cls : program.getClasses()) {
            for (Symbol sym : cls.getClassSymbolTable().getEntries().values()) {
                if (sym.getKind() == Kind.StaticMethod || sym.getKind() == Kind.VirtualMethod) {
                    if (sym.getNode() instanceof LibraryMethod) {
                        sym.setLirName("__" + sym.getName());
                    } else {
                        sym.setLirName("_" + cls.getName() + "_" + sym.getName());
                    }
                }
            }
        }

        List<String> classesCode = new LinkedList<String>();

        for (ICClass cls : program.getClasses()) {
            if (!cls.getName().equals("Library")) {
                List<String> classCode = (List<String>)cls.accept(this);
                classesCode.add("");
                classesCode.add("# Class Code for '" + cls.getName() + "'");
                classesCode.addAll(classCode);
            }
        }

        for (ICClass cls : program.getClasses()) {
            if (!cls.getName().equals("Library")) {
                programCode.add("# Class " + cls.getName() + " Offsets:");
                for (Field f : cls.getFields()) {
                    programCode.add("# " + f.getSymbolTableEntry().getSymbolOffset() + ": " + f.getName());
                }

                programCode.add("");
            }
        }

        this.stringLiteralsCode.add("str_error_division_by_zero: \"Runtime Error: Division by zero!\"");
        this.stringLiteralsCode.add("str_error_null_ref: \"Runtime Error: Null pointer dereference!\"");
        this.stringLiteralsCode.add("str_error_array_index_out_of_bounds: \"Runtime Error: Array index out of bounds!\"");
        this.stringLiteralsCode.add("str_error_array_neg_size: \"Runtime Error: Array allocation with negative array size!\"");

        programCode.add("# String Literals");
        programCode.addAll(this.stringLiteralsCode);
        programCode.add("");
        programCode.add("# Dispatch Tables");
        programCode.addAll(this.dispatchTables);
        programCode.addAll(classesCode);

        // generate lir entry point and error handling
        Method main = program.getMain();
        String argsLirName = main.getFormals().get(0).getSymbolTableEntry().getLirName();

        programCode.add("");
        programCode.add("# entry point");
        programCode.add("_ic_main:");
        programCode.add("StaticCall " + main.getSymbolTableEntry().getLirName() + "(" + argsLirName + "=0),Rdummy");
        programCode.add("Jump _exit");


        programCode.add("");
        programCode.add("# Error Handling");

        programCode.add("_error_division_by_zero:");
        programCode.add("Library __println(str_error_division_by_zero),Rdummy");
        programCode.add("Jump _exit");
        programCode.add("");

        programCode.add("_error_null_ref:");
        programCode.add("Library __println(str_error_null_ref),Rdummy");
        programCode.add("Jump _exit");
        programCode.add("");

        programCode.add("_error_array_index_out_of_bounds:");
        programCode.add("Library __println(str_error_array_index_out_of_bounds),Rdummy");
        programCode.add("Jump _exit");
        programCode.add("");

        programCode.add("_error_array_neg_size:");
        programCode.add("Library __println(str_error_array_neg_size),Rdummy");
        programCode.add("Jump _exit");

        programCode.add("");
        programCode.add("# jump here to end execution");
        programCode.add("_exit:");

        return programCode;
    }

    @Override
    public Object visit(ICClass icClass) {
        // build dispatch table
        List<String> classCode = new LinkedList<String>();

        SymbolTable classSymTable = icClass.getClassSymbolTable();
        String[] dispatchTable = new String[classSymTable.getDispatchTableSize()];

        while (classSymTable != null && classSymTable.getKind() == SymbolTableKind.Class) {
            for (Symbol sym : classSymTable.getEntries().values()) {
                if (sym.getKind() == Kind.VirtualMethod) {
                    //check if the method was was not overriden in subclass.
                    if (dispatchTable[sym.getSymbolOffset()] == null) {
                        dispatchTable[sym.getSymbolOffset()] = sym.getLirName();
                    }
                }
            }

            classSymTable = classSymTable.getParent();
        }

        StringBuilder dispatchTableStatement = new StringBuilder();
        dispatchTableStatement.append("_DV_").append(icClass.getName()).append(": [");
        String sep = "";
        for (String methodName : dispatchTable) {
            dispatchTableStatement.append(sep);
            dispatchTableStatement.append(methodName);
            sep = ",";
        }

        dispatchTableStatement.append("]");
        this.dispatchTables.add(dispatchTableStatement.toString());

        for (Method method : icClass.getMethods()) {
            List<String> methodCode = (List<String>)method.accept(this);
            classCode.add("");
            classCode.add("# Method Code for '" + method.getName() + "'");
            classCode.addAll(methodCode);
        }

        return classCode;
    }

    @Override
    public Object visit(Field field) {
        // Not called!
        return null;
    }

    private List<String> visitMethod(Method method) {
        List<String> methodCode = new LinkedList<String>();
        methodCode.add(method.getSymbolTableEntry().getLirName() + ":");

        for (Statement s : method.getStatements()) {
            List<String> sCode = (List<String>)s.accept(this);
            methodCode.addAll(sCode);
        }

        //for void methods and for non-void methods that do not return on all code paths (got a warning in context analysis).
        methodCode.add("Return 99999");

        return methodCode;
    }

    @Override
    public Object visit(VirtualMethod method) {
        return visitMethod(method);
    }

    @Override
    public Object visit(StaticMethod method) {
        return visitMethod(method);
    }

    @Override
    public Object visit(LibraryMethod method) {
        //we don't translate library methods
        return new LinkedList<String>();
    }

    @Override
    public Object visit(Formal formal) {
        // Not called!
        return null;
    }

    @Override
    public Object visit(PrimitiveType type) {
        // Not called!
        return null;
    }

    @Override
    public Object visit(UserType type) {
        // Not called!
        return null;
    }

    @Override
    public Object visit(Assignment assignment) {
        List<String> assignCode = new LinkedList<String>();

        int rightTarget = this.registerId;
        Expression exp = assignment.getAssignment();
        List<String> rightCode = (List<String>)exp.accept(this);
        assignCode.addAll(rightCode);
        this.registerId = rightTarget + 1;

        Location location = assignment.getVariable();

        if (location instanceof ArrayLocation) {
            Expression arrayExp = ((ArrayLocation) location).getArray();
            Expression arrayIndexExp = ((ArrayLocation) location).getIndex();

            int arrayExpTarget = this.registerId;
            List<String> arrayExpCode = (List<String>)arrayExp.accept(this);
            assignCode.addAll(arrayExpCode);
            this.registerId = arrayExpTarget + 1;

            int arrayIndexTarget = this.registerId;
            List<String> arrayIndexCode = (List<String>)arrayIndexExp.accept(this);
            assignCode.addAll(arrayIndexCode);
            this.registerId = arrayIndexTarget + 1;

            assignCode.add("Compare 0,R" + arrayExpTarget);
            assignCode.add("JumpTrue _error_null_ref");

            assignCode.add("Compare 0,R" + arrayIndexTarget);
            assignCode.add("JumpL _error_array_index_out_of_bounds");

            int lengthTarget = this.registerId;
            assignCode.add("ArrayLength R" + arrayExpTarget + ",R" + lengthTarget);
            this.registerId = lengthTarget + 1;

            assignCode.add("Compare R" + lengthTarget +",R" + arrayIndexTarget);
            assignCode.add("JumpGE _error_array_index_out_of_bounds");

            assignCode.add("MoveArray R" + rightTarget + ",R" + arrayExpTarget + "[R" + arrayIndexTarget + "]");
        } else if (location instanceof VariableLocation) {
            VariableLocation varLocation = (VariableLocation)location;
            Symbol varSym = varLocation.getSymbolTableEntry();

            if (varSym.getKind() == Kind.Field) {
                int instanceTarget = this.registerId;
                if (varLocation.isExternal()) {
                    List<String> externalCode = (List<String>)varLocation.getLocation().accept(this);
                    assignCode.addAll(externalCode);
                } else {
                    assignCode.add("Move this,R" + instanceTarget);
                }
                
                this.registerId = instanceTarget + 1;

                assignCode.add("Compare 0,R" + instanceTarget);
                assignCode.add("JumpTrue _error_null_ref");
                assignCode.add("MoveField R" + rightTarget + ",R" + instanceTarget + "." + varSym.getSymbolOffset());
            } else {
                assignCode.add("Move R" + rightTarget + "," + varSym.getLirName());
            }
        }

        this.registerId = rightTarget;
        return assignCode;
    }

    @Override
    public Object visit(CallStatement callStatement) {
        return callStatement.getCall().accept(this);
    }

    @Override
    public Object visit(Return returnStatement) {
        List<String> returnCode = new LinkedList<String>();
        
        if (returnStatement.hasValue()) {
            Expression retExp = returnStatement.getValue();
            int retTarget = this.registerId;
            List<String> returnExpCode = (List<String>)retExp.accept(this);
            this.registerId = retTarget + 1;

            returnCode.addAll(returnExpCode);
            returnCode.add("Return R" + retTarget);

            this.registerId = retTarget;
        } else {
            returnCode.add("Return 88888");
        }

        return returnCode;
    }

    @Override
    public Object visit(If ifStatement) {
        List<String> ifCode = new LinkedList<String>();
        this.uniqueId++;
        String endLabel = "_if_end_label" + this.uniqueId;
        String falseLabel = endLabel;
        if (ifStatement.hasElse()) {
            this.uniqueId++;
            falseLabel = "_if_else_label" + this.uniqueId;
        }

        int conditionTarget = this.registerId;
        List<String> conditionCode = (List<String>)ifStatement.getCondition().accept(this);
        this.registerId = conditionTarget + 1;
        ifCode.addAll(conditionCode);

        ifCode.add("Compare 0,R" + conditionTarget);
        ifCode.add("JumpTrue " + falseLabel);

        List<String> opCode = (List<String>)ifStatement.getOperation().accept(this);
        this.registerId = conditionTarget + 1;
        ifCode.addAll(opCode);

        if (ifStatement.hasElse()) {
            ifCode.add("Jump " + endLabel);
            ifCode.add(falseLabel + ":");
            List<String> elseOpCode = (List<String>)ifStatement.getElseOperation().accept(this);
            this.registerId = conditionTarget + 1;
            ifCode.addAll(elseOpCode);
        }

        ifCode.add(endLabel + ":");
        this.registerId = conditionTarget;

        return ifCode;
    }

    @Override
    public Object visit(While whileStatement) {
        List<String> whileCode = new LinkedList<String>();

        this.uniqueId++;
        String testLabel = "_while_test_label" + this.uniqueId;
        this.uniqueId++;
        String endLabel = "_while_end_label" + this.uniqueId;

        whileCode.add(testLabel + ":");

        int condTarget = this.registerId;
        List<String> condCode = (List<String>)whileStatement.getCondition().accept(this);
        this.registerId = condTarget + 1;

        whileCode.addAll(condCode);
        whileCode.add("Compare 0,R" + condTarget);
        whileCode.add("JumpTrue " + endLabel);

        String breakLabelBU = this.currentLoopBreakLabel;
        String continueLabelBU = this.currentLoopContinueLabel;

        this.currentLoopBreakLabel = endLabel;
        this.currentLoopContinueLabel = testLabel;
        List<String> opCode = (List<String>)whileStatement.getOperation().accept(this);
        this.registerId = condTarget + 1;

        this.currentLoopBreakLabel = breakLabelBU;
        this.currentLoopContinueLabel = continueLabelBU;

        whileCode.addAll(opCode);
        whileCode.add("Jump " + testLabel);
        whileCode.add(endLabel + ":");

        this.registerId = condTarget;

        return whileCode;
    }

    @Override
    public Object visit(Break breakStatement) {
        List<String> breakCode = new LinkedList<String>();
        breakCode.add("Jump " + this.currentLoopBreakLabel);
        return breakCode;
    }

    @Override
    public Object visit(Continue continueStatement) {
        List<String> breakCode = new LinkedList<String>();
        breakCode.add("Jump " + this.currentLoopContinueLabel);
        return breakCode;
    }

    @Override
    public Object visit(StatementsBlock statementsBlock) {
        List<String> blockCode = new LinkedList<String>();

        for (Statement s : statementsBlock.getStatements()) {
            List<String> sCode = (List<String>)s.accept(this);
            blockCode.addAll(sCode);
        }

        return blockCode;
    }

    @Override
    public Object visit(LocalVariable localVariable) {
        List<String> varCode = new LinkedList<String>();

        if (localVariable.hasInitValue()) {
            Symbol varSym = localVariable.getSymbolTableEntry();

            int initValueTarget = this.registerId;
            List<String> initValueCode = (List<String>)localVariable.getInitValue().accept(this);
            this.registerId = initValueTarget + 1;
            varCode.addAll(initValueCode);

            varCode.add("Move R" + initValueTarget + "," + varSym.getLirName());

            this.registerId = initValueTarget;
        }

        return varCode;
    }

    @Override
    public Object visit(VariableLocation varLocation) {
        List<String> varLocationCode = new LinkedList<String>();

        Symbol varSym = varLocation.getSymbolTableEntry();
        int varTarget = this.registerId;
        this.registerId = varTarget + 1;

        if (varSym.getKind() == Kind.Field) {
            int instanceTarget = this.registerId;
            if (varLocation.isExternal()) {
                List<String> externalCode = (List<String>)varLocation.getLocation().accept(this);
                varLocationCode.addAll(externalCode);
            } else {
                varLocationCode.add("Move this,R" + instanceTarget);
            }

            varLocationCode.add("Compare 0,R" + instanceTarget);
            varLocationCode.add("JumpTrue _error_null_ref");
            varLocationCode.add("MoveField R" + instanceTarget + "." + varSym.getSymbolOffset() + ",R" + varTarget);
        } else {
            varLocationCode.add("Move " + varSym.getLirName() + ",R" + varTarget);
        }

        this.registerId = varTarget;
        return varLocationCode;
    }

    @Override
    public Object visit(ArrayLocation location) {
        List<String> locationCode = new LinkedList<String>();

        Expression arrayExp = ((ArrayLocation) location).getArray();
        Expression arrayIndexExp = ((ArrayLocation) location).getIndex();

        int target = this.registerId;
        this.registerId++;

        int arrayExpTarget = this.registerId;
        List<String> arrayExpCode = (List<String>)arrayExp.accept(this);
        locationCode.addAll(arrayExpCode);
        this.registerId = arrayExpTarget + 1;

        int arrayIndexTarget = this.registerId;
        List<String> arrayIndexCode = (List<String>)arrayIndexExp.accept(this);
        locationCode.addAll(arrayIndexCode);
        this.registerId = arrayIndexTarget + 1;

        locationCode.add("Compare 0,R" + arrayExpTarget);
        locationCode.add("JumpTrue _error_null_ref");

        locationCode.add("Compare 0,R" + arrayIndexTarget);
        locationCode.add("JumpL _error_array_index_out_of_bounds");

        int lengthTarget = this.registerId;
        locationCode.add("ArrayLength R" + arrayExpTarget + ",R" + lengthTarget);
        this.registerId = lengthTarget + 1;

        locationCode.add("Compare R" + lengthTarget +",R" + arrayIndexTarget);
        locationCode.add("JumpGE _error_array_index_out_of_bounds");

        locationCode.add("MoveArray R" + arrayExpTarget + "[R" + arrayIndexTarget + "], R" + target);

        this.registerId = target;
        return locationCode;
    }

    private List<String> visitCall(Call call, Expression externalExp) {
        List<String> callCode = new LinkedList<String>();
        Symbol methodSym = call.getSymbolTableEntry();
        Method method = (Method)methodSym.getNode();
        MethodType methodType = (MethodType)methodSym.getType();

        int returnTarget = this.registerId;
        String targetRegister = "Rdummy";
        if (methodType.getReturnType() != this.typeTable.getVoidType()) {
            targetRegister = "R" + returnTarget;
        }

        int firstParamTarget = this.registerId;
        int currentParamTarget = this.registerId;
        for (int i = 0; i < call.getArguments().size(); i++) {
            Expression param = call.getArguments().get(i);
            List<String> paramCode = (List<String>)param.accept(this);
            callCode.addAll(paramCode);
            this.registerId = currentParamTarget + 1;
            currentParamTarget++;
        }

        StringBuilder callBuilder = new StringBuilder();

        if (method instanceof LibraryMethod) {
            callBuilder.append("Library ").append(methodSym.getLirName()).append("(");
            String sep = "";

            for (int i = 0; i < call.getArguments().size(); i++) {
                callBuilder.append(sep);
                callBuilder.append("R").append(firstParamTarget + i);
                sep = ",";
            }

            callBuilder.append("),").append(targetRegister);
        } else {

            if (method instanceof StaticMethod) {
                callBuilder.append("StaticCall ").append(methodSym.getLirName());
            } else if (method instanceof VirtualMethod) {

                int instanceTarget = this.registerId;
                if (externalExp == null) {
                    callCode.add("Move this,R" + instanceTarget);
                } else {
                    List<String> externalCode = (List<String>)externalExp.accept(this);
                    callCode.addAll(externalCode);
                }
                this.registerId = instanceTarget + 1;

                callCode.add("Compare 0,R" + instanceTarget);
                callCode.add("JumpTrue _error_null_ref");
                callBuilder.append("VirtualCall R").append(instanceTarget).append(".").append(methodSym.getSymbolOffset());
            }

            callBuilder.append("(");
            String sep = "";

            List<Formal> formals = method.getFormals();
            for (int i = 0; i < call.getArguments().size(); i++) {
                callBuilder.append(sep);
                callBuilder.append(formals.get(i).getSymbolTableEntry().getLirName());
                callBuilder.append("=R").append(firstParamTarget + i);
                sep = ",";
            }

            callBuilder.append("),").append(targetRegister);
        }

        callCode.add(callBuilder.toString());

        this.registerId = returnTarget;
        return callCode;
    }

    @Override
    public Object visit(StaticCall call) {
        return visitCall(call, null);
    }

    @Override
    public Object visit(VirtualCall call) {
        Expression externalExp = null;
        if (call.isExternal()) {
            externalExp = call.getLocation();
        }

        return visitCall(call, externalExp);
    }

    @Override
    public Object visit(This thisExpression) {
        List<String> thisCode = new LinkedList<String>();
        thisCode.add("Move this,R" + this.registerId);
        
        return thisCode;
    }

    @Override
    public Object visit(NewClass newClass) {
        List<String> allocCode = new LinkedList<String>();
        int objSize = ((ClassType)newClass.getContextType()).getIcClass().getClassSymbolTable().getInstanceTableSize() * 4;

        allocCode.add("Library __allocateObject(" + objSize + "),R" + this.registerId);
        allocCode.add("MoveField _DV_" + newClass.getName() + ",R" + this.registerId + ".0");

        return allocCode;
    }

    @Override
    public Object visit(NewArray newArray) {
        List<String> allocCode = new LinkedList<String>();
        
        int target = this.registerId;
        int sizeTarget = this.registerId;
        List<String> sizeCode = (List<String>)newArray.getSize().accept(this);
        this.registerId = sizeTarget + 1;
        allocCode.addAll(sizeCode);

        allocCode.add("Compare 0,R" + sizeTarget);
        allocCode.add("JumpL _error_array_neg_size");

        allocCode.add("Mul 4,R" + sizeTarget);
        allocCode.add("Library __allocateArray(R" + sizeTarget + "),R" + target);
        
        this.registerId = target;
        return allocCode;
    }

    @Override
    public Object visit(Length length) {
        List<String> lengthCode = new LinkedList<String>();

        int target = this.registerId;
        int arrayTarget = this.registerId;
        
        List<String> arrayCode = (List<String>)length.getArray().accept(this);
        this.registerId = arrayTarget + 1;
        lengthCode.addAll(arrayCode);

        lengthCode.add("Compare 0,R" + arrayTarget);
        lengthCode.add("JumpTrue _error_null_ref");
        lengthCode.add("ArrayLength R" + arrayTarget + ",R" + target);

        this.registerId = target;
        return lengthCode;
    }

    @Override
    public Object visit(MathBinaryOp binaryOp) {
        List<String> binaryOpCode = new LinkedList<String>();

        int target = this.registerId;

        int operand1Target = target;
        List<String> operand1Code = (List<String>)binaryOp.getFirstOperand().accept(this);
        this.registerId = target + 1;
        binaryOpCode.addAll(operand1Code);

        int operand2Target = this.registerId;
        List<String> operand2Code = (List<String>)binaryOp.getSecondOperand().accept(this);
        this.registerId = operand2Target + 1;
        binaryOpCode.addAll(operand2Code);

        switch (binaryOp.getOperator()) {
            case PLUS:
                if (binaryOp.getFirstOperand().getContextType() == this.typeTable.getStringType()) {
                    binaryOpCode.add("Library __stringCat(R" + operand1Target + ",R" + operand2Target + "),R" + target);
                } else {
                    binaryOpCode.add("Add R" + operand2Target + ",R" + operand1Target);
                }
                break;
            case MINUS:
                binaryOpCode.add("Sub R" + operand2Target + ",R" + operand1Target);
                break;
            case MULTIPLY:
                binaryOpCode.add("Mul R" + operand2Target + ",R" + operand1Target);
                break;
            case DIVIDE:
                binaryOpCode.add("Compare 0,R" + operand2Target);
                binaryOpCode.add("JumpTrue _error_division_by_zero");
                binaryOpCode.add("Div R" + operand2Target + ",R" + operand1Target);
                break;
            case MOD:
                binaryOpCode.add("Compare 0,R" + operand2Target);
                binaryOpCode.add("JumpTrue _error_division_by_zero");
                binaryOpCode.add("Mod R" + operand2Target + ",R" + operand1Target);
                break;
        }

        this.registerId = target;

        return binaryOpCode;
    }

    @Override
    public Object visit(LogicalBinaryOp binaryOp) {
        List<String> binaryOpCode = new LinkedList<String>();

        int target = this.registerId;

        List<String> operand1Code = (List<String>)binaryOp.getFirstOperand().accept(this);
        this.registerId = target + 1;
        binaryOpCode.addAll(operand1Code);

        this.uniqueId++;
        String endLabel = "_binop_end_label" + this.uniqueId;

        if (binaryOp.getOperator() == BinaryOps.LAND) {
            binaryOpCode.add("Compare 0,R" + target);
            binaryOpCode.add("JumpTrue " + endLabel);
        } else if (binaryOp.getOperator() == BinaryOps.LOR) {
            binaryOpCode.add("Compare 1,R" + target);
            binaryOpCode.add("JumpTrue " + endLabel);
        }

        int operand2Target = this.registerId;
        List<String> operand2Code = (List<String>)binaryOp.getSecondOperand().accept(this);
        this.registerId = operand2Target + 1;
        binaryOpCode.addAll(operand2Code);

        switch (binaryOp.getOperator()) {
            case LAND:
                binaryOpCode.add("And R" + operand2Target + ",R" + target);
                break;
            case LOR:
                binaryOpCode.add("Or R" + operand2Target + ",R" + target);
                break;
            default:
                int operand1Target = this.registerId;
                binaryOpCode.add("Move R" + target + ",R" + operand1Target);
                this.registerId = operand1Target + 1;

                binaryOpCode.add("Move 1,R" + target);
                binaryOpCode.add("Compare R" + operand2Target + ",R" + operand1Target);

                // When using "Compare Rx, Ry"
                // "JumpL" jumps on Ry < Rx
                switch (binaryOp.getOperator()) {
                    case LT:
                        binaryOpCode.add("JumpL " + endLabel);
                        break;
                    case LTE:
                        binaryOpCode.add("JumpLE " + endLabel);
                        break;
                    case GT:
                        binaryOpCode.add("JumpG " + endLabel);
                        break;
                    case GTE:
                        binaryOpCode.add("JumpGE " + endLabel);
                        break;
                    case EQUAL:
                        binaryOpCode.add("JumpTrue " + endLabel);
                        break;
                    case NEQUAL:
                        binaryOpCode.add("JumpFalse " + endLabel);
                        break;
                }
                binaryOpCode.add("Move 0,R" + target);
                break;
        }

        binaryOpCode.add(endLabel + ":");

        this.registerId = target;

        return binaryOpCode;
    }

    @Override
    public Object visit(MathUnaryOp unaryOp) {
        List<String> unaryOpCode = new LinkedList<String>();

        int target = this.registerId;

        if (unaryOp.getOperator() == UnaryOps.UMINUS) {
            //check for overflow
            if (unaryOp.getOperand() instanceof  Literal) {
                Literal literal = (Literal)unaryOp.getOperand();
                if (literal.getType() == LiteralTypes.INTEGER &&
                        ((Long)literal.getValue()) == 0x80000000L) {
                    unaryOpCode.add("Move " + 0x80000000L + ",R" + target);
                    unaryOpCode.add("Neg R" + target);
                    this.registerId = target;
                    return unaryOpCode;
                }
            }

            List<String> operandCode = (List<String>)unaryOp.getOperand().accept(this);
            unaryOpCode.addAll(operandCode);

            unaryOpCode.add("Neg R" + target);
        }

        this.registerId = target;

        return unaryOpCode;
    }

    @Override
    public Object visit(LogicalUnaryOp unaryOp) {
        List<String> unaryOpCode = new LinkedList<String>();

        int target = this.registerId;

        if (unaryOp.getOperator() == UnaryOps.LNEG) {

            List<String> operandCode = (List<String>)unaryOp.getOperand().accept(this);
            unaryOpCode.addAll(operandCode);

            unaryOpCode.add("Xor 1,R" + target);
        }

        this.registerId = target;

        return unaryOpCode;
    }

    @Override
    public Object visit(Literal literal) {
        List<String> literCode = new LinkedList<String>();

         LiteralTypes type = literal.getType();
        switch (type) {
            case INTEGER:
                literCode.add("Move " + literal.getValue() + ",R" + this.registerId);
                break;
            case STRING:
                String literalLbl;
                String stringValue = literal.getValue().toString();

                if (this.stringLiterals.containsKey(stringValue)) {
                    literalLbl = this.stringLiterals.get(stringValue);
                } else {
                    this.uniqueId++;
                    literalLbl = "str" + this.uniqueId;
                    this.stringLiteralsCode.add(literalLbl + ": " + stringValue);
                    this.stringLiterals.put(stringValue, literalLbl);
                }

                literCode.add("Move " + literalLbl + ",R" + this.registerId);
                break;
            case TRUE:
                literCode.add("Move 1,R" + this.registerId);
                break;
            case FALSE:
            case NULL:
                literCode.add("Move 0,R" + this.registerId);
                break;
        }

        return literCode;
    }

    @Override
    public Object visit(ExpressionBlock expressionBlock) {
        return expressionBlock.getExpression().accept(this);
    }
}
